# Copyright (c) 2007 Fabio Makoto Akita
# 
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Author: AkitaOnRails (www.akitaonrails.com)
# Support: Surgeworks LLC (www.surgeworks.com)

#
# WARNING: ONLY EDIT THIS FILE MANUALLY - IT IS AUTOMATICALLY GENERATED BY THE PLUGIN
#

# This module represents all operations done at both the client and server ends.
# Here follows the basic workflow covered by this set of modules. All of the
# modules declared here are used in the SyncsController only
#
# Client:
# 1 - perform_sync
#   2 - handshake_authenticate
#     4 - handshake_response
#   7 - do_get_server_data
#     9 - Replica.up
#   10 - do_send_client_data
#     11 - Replica.down
#
# Server:
# 3 - handshake(GET)
# 5 - handshake(POST)
#   6 - handshake_response
# 8 - down.yaml(GET)
# 12 - up.yaml(POST)
module SyncsHelper
  # This is available in the alpha versions just for testing and debugging because
  # the main up and down actions only responde to Post requests. But this should
  # disappear in the next versions, so don't ever rely on them
  module Tests
    def teste_down
      config_file = File.join(RAILS_ROOT, 'vendor', 'plugins', 'acts_as_replica', 'test', 'results', 'syncs.yml')
      sync_package = Replica.down(params[:start_id] || -1, $SYNC_CONFIG[:unique_machine_id]).to_yaml
      File.open(config_file, 'w') { |f| f.write sync_package }
      render :text => sync_package
    end

    def teste_up
      config_file = File.join(RAILS_ROOT, 'vendor', 'plugins', 'acts_as_replica', 'test', 'results', 'syncs.yml')
      result_file = File.join(RAILS_ROOT, 'vendor', 'plugins', 'acts_as_replica', 'test', 'results', 'syncs_result.yml')
      body = YAML::load(IO.read(config_file))
      result = Replica.up(body).to_yaml
      File.open(result_file, 'w') { |f| f.write result }
    end
  end
  
  # Here are the main actions.
  # - perform_sync: orchestrates the whole operation
  # - down: the server sends its most recent data to the requesting client
  # - up: the server receives newer data from the client and records them locally
  module Actions  
    # Client call that initiates the syncing process
    # probably better to override this method and defer this processing to
    # a asynchronous task using tools as BackgroundDrb, AP4R, daemon_generate or other
    def perform_sync
      run_sync
    end

    # Server returns the last modified records, as required by the
    # client, using the :start_id param to know where to start.
    #
    # This should not be use directly. 
    # Use the do_get_server_data action to operate over it.
    def down
      if request.post?
        logger.info "/down.yaml - session: #{session[:client_id]} - params: #{params.to_yaml}"
        count = params[:count] == 'true'
        package = Replica.down(params[:start_id], session[:machine].machine_id, :count => count).to_yaml
        logger.info "result: #{package}"
        render :text => package, :content_type => 'text/plain'
      else
        render :text => '', :status => 400
      end
    end

    # Server receives the client's data, load them into its own database
    # and records the last time.
    #
    # This should not be used directly.
    # Use the do_send_client_data action to operate over it
    def up
      if request.post?
        # loads the models before unmarshalling the received payload
        fix_model_loading
        # operates over each transaction in the payload
        result = Replica.up(YAML.load(params[:syncs]), 
          :machine_id => session[:machine].machine_id)
        unless result.errors and result.errors.size > 0
          # if everything went ok, stamp the clients local data with
          # the most recent timestamp that came back from the client
          # so we don't mix client and server timestamps timezones 
          # and other clock related misconfigurations
          stamp_sync_remote_user result.last_synced_id if result.last_synced_id
          render :text => result.to_yaml, :status => 200
        else
          render :text => result.to_yaml, :status => 500
        end
      else
        render :text => '', :status => 400
      end
    end
    
    private 
    
    # create or delete the process semaphore
    def semaphore_deal(create = true)
      semaphore_file = File.join(RAILS_ROOT, $SYNC_CONFIG[:semaphore_file])
      unless create
        File.delete(semaphore_file) if File.exists?(semaphore_file)
        return true
      end
      can_create = true
      if File.exists?(semaphore_file)
        timestamp = nil
        File.open(semaphore_file, 'r') { |f| timestamp = YAML.load(f.gets) }
        can_create = (Time.now - timestamp) / 3600 >= 1.0
      end
      File.open( semaphore_file, 'w' ) { |f| f.write Time.now.to_yaml } if can_create
      can_create
    rescue => e
      logger.error "Couldn't create semaphore file. #{e.to_yaml}"
      false
    end

    # run the complete download procedure, with batch support. 
    # f is the log file handle
    def run_download(server_machine_id, f)
      count = do_get_server_data_count(address, server_machine_id)
      f.puts "#{log_timestamp} --- ESTIMATED TOTAL RECORDS TO DOWNLOAD: #{count.total}"
      f.flush
      total = 0
      while true
        begin
          result_up = do_get_server_data(address, server_machine_id)
          if result_up.total > 0
            total += result_up.total
            f.puts "#{log_timestamp} -- DOWNLOADED AND PROCESSED: #{total} RECORDS"
            if result_up.errors.size > 0
              f.puts "#{log_timestamp} -- DOWNLOAD STATUS: #{result_up.errors.total}" 
              logger.error result_up.errors.to_yaml
            end
            f.flush
          else
            break
          end
        rescue => e
          logger.error "#{log_timestamp} -- PROBLEMS DOWNLOADING: #{e.to_yaml}"
        end
      end
      total
    end
      
    # run the complete upload procedure, with batch support. 
    # server_result is the last synced id got from the server in the handshake procedure
    # f is the log file handle
    def run_upload(server_result, server_machine_id, f)
      count = Replica.down(server_result, server_machine_id, :count => true)
      f.puts "#{log_timestamp} --- ESTIMATED TOTAL RECORDS TO UPLOAD: #{count.total}"
      f.flush
      total = 0
      while true
        begin
          result_down = do_send_client_data(address, server_result, server_machine_id)
          server_result = result_down.last_synced_id if result_down.respond_to?(:last_synced_id)
          if result_down.total > 0
            total += result_down.total
            f.puts "#{log_timestamp} -- UPLOADED AND PROCESSED: #{total} RECORDS"
            if result_down.errors.size > 0
              f.puts "#{log_timestamp} -- UPLOAD STATUS: #{result_down.errors.total}" 
              logger.error result_down.errors.to_yaml
            end
            f.flush
          else
            break
          end
        rescue => e
          logger.error "#{log_timestamp} -- PROBLEMS UPLOADING: #{e.to_yaml}"
        end
      end
      total
    end

    def run_sync
      @config = $SYNC_CONFIG
      
      # deal with the semaphore
      has_semaphore = semaphore_deal

      log_file = delete_log
      total_down, total_up = 0, 0
      File.open(log_file, 'a') do |f|
        start_time = Time.now
        f.puts "#{log_timestamp} --- REPLICATION STARTED."
        f.flush
        begin
          if (server_package = handshake_authenticate(address))
            logger.info "-- HANDSHAKE FINAL PACKAGE: #{server_package.to_yaml}"
            server_result = server_package[:last_synced]
            server_machine_id = server_package[:server_machine_id]
            total_down = run_download( server_machine_id, f )
            total_up = run_upload( server_result, server_machine_id, f )
          else
            f.puts "#{log_timestamp} --- ERROR: PROBLEMS AUTHENTICATION. WRONG USERNAME?"
            f.flush
          end
        rescue => e
          f.puts "#{log_timestamp} --- ERROR: CANNOT REACH SERVER AT #{address}. ERROR: #{e.to_yaml}"
          f.flush
        end
        end_time = Time.now
        total_time = log_fmt_timestamp(end_time - start_time)
        f.puts "#{log_timestamp} --- REPLICATION FINISHED IN #{total_time} HOURS. #{total_down} RECORDS DOWNLOADED. #{total_up} RECORDS UPLOADED."
        f.flush
      end
      semaphore_deal(false) # deletes semaphore
    end
  end
  
  # This process has 3 major steps, this is the first: the handshake operation
  # is comprised of 2 main steps: requesting a challenge from the server, and
  # sending back the response. Upon authentication, the server sends back the
  # most recent timestamp from the last successful syncing
  module Handshake
    # the server answers to a 2-way handshake process
    # the GET answers with a challenge-response token
    # the POST compares the received response
    #  if OK, sends back last synced time record in the server
    def handshake
      if request.get?
        session[:challenge] = UUID.timestamp_create.to_s
        render :text => session[:challenge], :status => 200
      else
        # for the server to compare the response, the client has to send back
        # the http cookie for the server to retrieve the original challenge
        if handshake_response(session[:challenge], params[:client_id], false) == params[:challenge_response]
          session[:client_id] = params[:client_id]
          # the user is authenticated as a RemoteClient, now has to associate with the
          # machine id sent. If it doesn't exist, create a new pairing. This means the
          # Replica table in the other side is also brand new, starting from ID 1 and
          # we will retrieve everything back to the server
          session[:machine] = current_remote_client.remote_machines.find_by_machine_id(params[:machine_id])
          unless session[:machine]
            session[:machine] = RemoteMachine.create :machine_id => params[:machine_id],
              :remote_client => current_remote_client
          end
          token = session[:machine].last_synced || -1
          render :text => {:last_synced => token, 
            :server_machine_id => SyncSetting.load.unique_machine_id}.to_yaml, 
            :status => 200, :content_type => 'application/x-yaml'
        else
          session[:client_id] = nil
          render :text => "Incorrect Challenge-Response", :status => 403
        end
      end
    end

    private
    # before_filter for synchronization
    # Expects to have session[:user] populated with the remote client user id.
    # This is already configured in the SyncsController class
    def authenticated?
      # if this is the client, check :user, if this is the server, check :client_id
      if session[:user].nil? and session[:client_id].nil?
        render :text => "User not logged in", :status => 403
        return false
      end
      true
    end
  
    # Executes a remote handshake with challenge-response authentication
    # this is to be called from the client's end, in the perform_sync action
    #
    # - Client requests a challenge
    # - Client uses the challenge with its internal guid and generates a SHA1 response
    # - Client sends the response back
    # - Client receives back the last successful synced time from the server
    #
    # Can be configured in the syncable.yml under :handshake_uri
    def handshake_authenticate(address)
      # request the challenge
      req = http_open_get(address, $SYNC_CONFIG[:handshake_uri])
      case req
      when Net::HTTPSuccess
        # generate response and send back with user_id
        resp = handshake_response(req.body, current_client.id)
        req = http_open_post(address, $SYNC_CONFIG[:handshake_uri]) do |request|
          request.set_form_data({
            'client_id'          => current_client.id, 
            'challenge_response' => resp, 
            'machine_id'         => $SYNC_CONFIG[:unique_machine_id] })
        end
        case req
        when Net::HTTPSuccess
          return YAML.load(req.body)
        end
      end    
      nil
    end
  
    # Helper for both the client and server to calculate
    # the response digest from the challenge key
    def handshake_response(challenge, client_id, client = true)
      if client
        u = User.find(client_id)
      else
        u = RemoteClient.find_by_client_id(client_id)
      end
      Digest::SHA1.hexdigest("#{challenge}-#{client_id}-#{u.guid}")
    end
    
  end
  
  # There are 3 major operations in this plugin, here is the second and third majors:
  # the 'receiving' and 'sending' operations. The first brings data from the server
  # and the second sends local data to the server. In the end, both databases should
  # have the same ammount of rows for each of the replicated tables
  module Request
    private
    # Client calls the remote down.yaml with special :count => true flag and gets data 
    # count from the server Replica.down method. This is useful to call before do_get_server_data
    # so it can be measured how much data is about to be downloaded
    def do_get_server_data_count(address, server_machine_id)
      do_get_server_data address, server_machine_id, true
    end

    # Client calls the remote down.yaml and gets data from the server Replica.down method,
    # then the client uses this data to update its own database through the internal
    # Replica.up method.
    def do_get_server_data(address, server_machine_id, count = false)
      # call the remote server sending the last time the syncing was made, so it
      # gets only the data it doesn't already have
      req = http_open_post(address, $SYNC_CONFIG[:down_uri]) do |request|
        start_id = current_client.last_synced || -1
        form_data = { 'start_id' => start_id, 'machine_id' => $SYNC_CONFIG[:unique_machine_id] }
        form_data.merge!('count' => 'true') if count
        request.set_form_data form_data
      end
      # unpacks the received YAML package and start operating through each
      # received transaction in the payload
      logger.info "Received data: [[[#{req.body}]]]"
      result ||= ActsAsReplica::Structs::SyncUpResult.new
      result.status = case req
      when Net::HTTPSuccess
        fix_model_loading
        package = YAML.load(req.body)
        if Replica.compare_version(package.version)
          result.errors << "Remote #{package.version} and Local #{Replica.version} versions are different."
          500
        elsif !count && package.total.to_i > 0
          result = Replica.up(package, :machine_id => server_machine_id)
          # upon completion, stamps the latest timestam returned in the most
          # recent transaction in the payload package. Takes care to use the
          # server's date as the reference, so to avoid conflicting timezones
          # and misconfigured local clocks
          current_client.update_attributes( :last_synced => result.last_synced_id ) if result.last_synced_id
        else
          result = package
        end
        200
      else
        500
      end
      result
    end

    # After the successful Handshake operation, the server is able to determine
    # the last time it received new data from this particular client. So it sends
    # back this timestamp to be used here. Then the client can get the most recent
    # data to send back to it.
    def do_send_client_data(address, start_id, server_machine_id)
      # Get the latest local data
      package = Replica.down(start_id, server_machine_id, :count => false)
      return ActsAsReplica::Structs::SyncUpResult.new(:total => 0, :status => 200, 
        :last_synced_id => start_id) if package.total == 0

      # Sends the local data packaged as YAML
      req = http_open_post(address, $SYNC_CONFIG[:up_uri]) do |request|
        #request.content_type = 'application/x-yaml'
        request.content_type = 'text/plain'
        request.set_form_data('syncs' => package.to_yaml)
        logger.info "Sending data: [[[#{package.to_yaml}]]]"
      end
      # process the returned status from the server
      begin
        result = YAML.load(req.body)
      rescue => e
        result = ActsAsReplica::Structs::SyncUpResult.new :errors => [e], 
          :status => 500, :last_synced_id => start_id
      end
      result
    end  
  end
  
  # All communication is held through HTTP encapsulation. This support relies
  # on Ruby's own Net:HTTP library. This is just a helper to make it easier to
  # make such connections
  module NetSupport
    private
    
    # GET helper for http_open
    def http_open_get(address, uri, &block)
      http_open 'get', address, uri, &block
    end

    # POST helper for http_open
    def http_open_post(address, uri, &block)
      http_open 'post', address, uri, &block
    end

    # Encapsulates Proxy authentication, Basic HTTP authentication, 
    # Cookie handling, and SSL all at the same time 
    def http_open(method, address, uri, &block)
      sURI = URI.parse(address) 
      # deal if HTTP proxies
      http_conn = if $SYNC_CONFIG[:proxy_host]
        Net::HTTP.new(sURI.host, sURI.port, $SYNC_CONFIG[:proxy_host], $SYNC_CONFIG[:proxy_port], $SYNC_CONFIG[:proxy_user], $SYNC_CONFIG[:proxy_pass])
      else
        Net::HTTP.new(sURI.host, sURI.port)
      end
      # deal if SSL
      if $SYNC_CONFIG[:protocol] == 'https'
        http_conn.use_ssl = true 
        http_conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
      end
      # deal with cookie trafic
      url_header = nil
      if session['server-cookie']
        url_header = {'Cookie' => session['server-cookie']} 
        logger.info "SENDING COOKIE: #{url_header.to_yaml}"
      end
      req = http_conn.start do |http|
        if method == 'get'
          request = Net::HTTP::Get.new(sURI.path + uri, url_header)
        else
          request = Net::HTTP::Post.new(sURI.path + uri, url_header) 
        end
        # deal with HTTP Basic Authentication
        request.basic_auth($SYNC_CONFIG[:auth_user], $SYNC_CONFIG[:auth_pass]) unless $SYNC_CONFIG[:auth_user].nil?
      
        block.call(request) if block
        logger.info "Request Info: #{request.to_yaml}"
        http.request(request)
      end
      session['server-cookie'] = req.response['set-cookie'].split('; ')[0] unless session['server-cookie']
      logger.info "Requested: #{method} #{address}#{uri}"
      logger.info "Result: #{req.to_yaml}"
      req
    end
  end
  
  module Misc
    private
    
    # Assumes the existence of a User model binded to the Replica model
    # the same way UserStamp works
    def current_client
      @current_client ||= Replica.user_model.find(session[:user])
      @current_client
    end
  
    # This is used in the 'server' end of the communication, where a server
    # can serve lots of 'clients', each one registered with a unique
    # identifier in the RemoteClient table
    def current_remote_client
      return nil unless session[:client_id]
      @remote_client ||= RemoteClient.find_by_client_id(session[:client_id])
      @remote_client
    end
  
    # The server logs in when was the last time it received data
    # from a specific client. It send this time back after the
    # next successful authentication handshake
    def stamp_sync_remote_user(t)
      machine = RemoteMachine.find(session[:machine].id)
      machine.last_synced = t || -1
      machine.update
    rescue => e
      raise "Error assigning new last_synced timestamp. #{e.to_yaml}"
    end
  
    # This pre-loads the models so YAML deserialization finds the correct classes
    # but still dunno why this is required. If don't used YAML will deserialize
    # model objects as YAML::Object instances
    def fix_model_loading
      Dir.new(File.join(RAILS_ROOT, 'app', 'models')).each { |s| require s if s =~ /\.rb$/ }
    end
  
    # Helper to build the HTTP address through the $SYNC_CONFIG parameters
    def address
      "#{$SYNC_CONFIG[:protocol]}://#{$SYNC_CONFIG[:hostname]}:#{$SYNC_CONFIG[:port]}"
    end
    
    # timestamp for the sync logs
    def log_timestamp
      Time.now.iso8601
    end
    
    # formats a given interval Time - Time, in 00:00:00 format
    def log_fmt_timestamp(time)
      [(time/3600).to_i, (time/60 % 60).to_i, (time % 60).to_i].map{|t| t.to_s.rjust(2,'0')}.join(':')
    end
    
    def delete_log
      # reset and starts log file
      log_file = File.join(RAILS_ROOT, $SYNC_CONFIG[:log_file])
      File.delete(log_file) if File.exists?(log_file) && $SYNC_CONFIG[:wipe_log]
      log_file
    end
  end
end